Skip to content

Conversation

@sieniven
Copy link
Contributor

@sieniven sieniven commented Dec 3, 2025

📝 Summary

This PR resolve issues found on the payload handler when handling received externally built flashblocks payload. It is found that when retrieving external payloads from peer builders, the state root calculation could mismatch due to errors on the payload validation execution (refer to Issue logs section for the example logs).

  1. Resolve issue where retrieval depositor nonce was after tx execution - depositor nonce should be cached and retrieved before the transaction execution
  2. Use the standard reth's execution pattern evm.transact() instead for execution of transactions and handling of state changes. We should pass the tx value itself directly into evm.transact, allowing the safe conversion into the tx env interface (for revm execution) using upstream conversion methods
  3. Add pre-execution payload validation on the incoming block header and parent block header validations, aligning with the default reth's payload_validator logic
  4. Chore: switch repeated logs to debug level

Issue logs

2025-12-03T06:24:06.957584Z INFO Received block from consensus engine number=8594186 hash=0x92a82266795a15929a3ed696fe1e362172b1d63d50baf46beefa7342329200a8
2025-12-03T06:24:06.958276Z INFO Canonical chain committed number=8594186 hash=0x92a82266795a15929a3ed696fe1e362172b1d63d50baf46beefa7342329200a8 elapsed=56.792µs
2025-12-03T06:24:07.959807Z INFO executing flashblock header=Header { parent_hash: 0x92a82266795a15929a3ed696fe1e362172b1d63d50baf46beefa7342329200a8, ommers_hash: 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347, beneficiary: 0x4200000000000000000000000000000000000011, state_root: 0xddfd9afcfde543996f2d18e8f5c62e57d232e0f0e9064940edaaa2d224871237, transactions_root: 0x83d6757ffa229f27347d6c365e942198163325eb8c1b1a5c159a9d844422b581, receipts_root: 0xfc1d8bc99328e4113633b9ab891882bac50660e419d13c144d943f7c811e38d1, logs_bloom: 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, difficulty: 0, number: 8594187, gas_limit: 150000000, gas_used: 147220286, timestamp: 1764743048, extra_data: 0x00000000fa00000006, mix_hash: 0x9d542a3f5333b4b3f8223c60b866bc33a4fd4be8ee3e4b3e7c6cb527b6ec18fa, nonce: 0x0000000000000000, base_fee_per_gas: Some(345052862), withdrawals_root: Some(0x8ed4baae3a927be3dea54996b4d5899f8c01e7594bf50b17dc1e741388ce3d12), blob_gas_used: Some(0), excess_blob_gas: Some(0), parent_beacon_block_root: Some(0x8b8d851a12382805c09ed58c23047e2888b3f6649d9b3952383922b89e68efe6), requests_hash: Some(0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855) }
2025-12-03T06:24:07.983973Z INFO Received block from consensus engine number=8594187 hash=0x028840ebb9422a1f70ccb6b92ac91b99ba0c714484f2914f8fed9235b84e32e7
2025-12-03T06:24:08.058053Z ERROR flashblock hash mismatch after execution expected=0x028840ebb9422a1f70ccb6b92ac91b99ba0c714484f2914f8fed9235b84e32e7 got=0xfbf5b5d69ff651ce69d1028c6e5912f471d565290378d1d91e6827b053535b9e
2025-12-03T06:24:08.058826Z ERROR failed to execute received flashblock error=flashblock hash mismatch after execution

✅ I have completed the following steps:

  • [✅] Run make lint
  • [✅] Run make test
  • Added tests (if applicable)

@SozinM
Copy link
Collaborator

SozinM commented Dec 11, 2025

@noot

@sieniven
Copy link
Contributor Author

@noot @SozinM hey guys, any updates for this PR?

@noot
Copy link
Contributor

noot commented Jan 15, 2026

hi @sieniven thank you for the fixes, apologies for the slow response! are you able to share the steps you used to trigger the hash mismatch error? this feature is very experimental and has only been tested locally, not on any sort of production environment. it would be good to have the cases you used for testing documented.

@sieniven
Copy link
Contributor Author

hi @sieniven thank you for the fixes, apologies for the slow response! are you able to share the steps you used to trigger the hash mismatch error? this feature is very experimental and has only been tested locally, not on any sort of production environment. it would be good to have the cases you used for testing documented.

Hey @noot, thanks for taking the time to explain - this makes sense. The hash mismatch error occur in 2 scenarios. I'll comment directly on the code blocks for your reference.

Comment on lines -297 to -365
let sender = tx
.recover_signer()
.wrap_err("failed to recover tx signer")?;
let tx_env = TxEnv::from_recovered_tx(&tx, sender);
let executable_tx = match tx {
OpTxEnvelope::Deposit(ref tx) => {
let deposit = DepositTransactionParts {
mint: Some(tx.mint),
source_hash: tx.source_hash,
is_system_transaction: tx.is_system_transaction,
};
OpTransaction {
base: tx_env,
enveloped_tx: None,
deposit,
}
}
OpTxEnvelope::Legacy(_) => {
let mut tx = OpTransaction::new(tx_env);
tx.enveloped_tx = Some(vec![0x00].into());
tx
}
OpTxEnvelope::Eip2930(_) => {
let mut tx = OpTransaction::new(tx_env);
tx.enveloped_tx = Some(vec![0x00].into());
tx
}
OpTxEnvelope::Eip1559(_) => {
let mut tx = OpTransaction::new(tx_env);
tx.enveloped_tx = Some(vec![0x00].into());
tx
}
OpTxEnvelope::Eip7702(_) => {
let mut tx = OpTransaction::new(tx_env);
tx.enveloped_tx = Some(vec![0x00].into());
tx
}
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first problematic area that the PR resolves is here - there shouldnt be a need to write local definition of convertion methods to alloy typed tx types, but instead pass the tx value itself directly into evm.transact, which allows the safe conversion into the tx env interface (for revm execution) using upstream conversion methods.

.transpose()
.wrap_err("failed to get depositor nonce")?;

let ResultAndState { result, state } = match evm.transact_raw(executable_tx) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to use the upstream's default pattern, evm.transact here which inherently calls IntoTxEnv on the concrete type for tx execution.

Comment on lines -368 to -406
let depositor_nonce = (is_regolith_active && tx.is_deposit())
.then(|| {
evm.db_mut()
.load_cache_account(sender)
.map(|acc| acc.account_info().unwrap_or_default().nonce)
})
.transpose()
.wrap_err("failed to get depositor nonce")?;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second issue (and where the block hash mismatch is) is located here. The depositor nonce when bulding the receipt should be using the nonce prior to execution. However, the deposit tx receipt uses the depositor nonce after execution, which will cause a stateroot mismatch on the incorrect block data

Comment on lines 394 to 419
/// Validates the payload header and its relationship with the parent before execution.
/// This performs consensus rule validation including:
/// - Header field validation (timestamp, gas limit, etc.)
/// - Parent relationship validation (block number increment, timestamp progression)
fn validate_pre_execution(
payload: &OpBuiltPayload,
parent_header: &reth_primitives_traits::Header,
parent_hash: alloy_primitives::B256,
chain_spec: Arc<OpChainSpec>,
) -> eyre::Result<()> {
use reth::consensus::HeaderValidator;

fn is_regolith_active(chain_spec: &OpChainSpec, timestamp: u64) -> bool {
use reth_optimism_chainspec::OpHardforks as _;
chain_spec.is_regolith_active_at_timestamp(timestamp)
let consensus = OpBeaconConsensus::new(chain_spec);
let parent_sealed = SealedHeader::new(parent_header.clone(), parent_hash);

// Validate incoming header
consensus
.validate_header(payload.block().sealed_header())
.wrap_err("header validation failed")?;

// Validate incoming header against parent
consensus
.validate_header_against_parent(payload.block().sealed_header(), &parent_sealed)
.wrap_err("header validation against parent failed")?;

Ok(())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 3rd issue is that in the payload validation process on default reth, we should pre-validate the incoming payload first prior to execution. This includes validating incoming payload header first which is missing in this p2p builder validation logic - https://github.com/okx/reth/blob/upstream/dev/crates/engine/tree/src/tree/payload_validator.rs#L27

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants